﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

namespace PerfectSphere
{
    internal partial class MainForm : Form
    {
        private delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);

        public MainForm()
        {
            InitializeComponent();

            this.Text += " " + this.GetType().Assembly.GetName().Version.ToString();
        }

        private static unsafe void Write4BitIndexedPixel(BitmapData blueprintData, int x, int y, int colorIndex)
        {
            int offset = (y * blueprintData.Stride) + (x >> 1);
            byte currentByte = ((byte*)blueprintData.Scan0)[offset];
            if ((x & 1) == 1)
            {
                currentByte &= 0xF0;
                currentByte |= (byte)(colorIndex & 0x0F);
            }
            else
            {
                currentByte &= 0x0F;
                currentByte |= (byte)(colorIndex << 4);
            }
            ((byte*)blueprintData.Scan0)[offset] = currentByte;
        }

        private IEnumerable<Block> CreateElipsoid(int diameterX, int diameterY, int diameterZ)
        {
            float radiusX = diameterX / 2f;
            float radiusY = diameterY / 2f;
            float radiusZ = diameterZ / 2f;

            float radiusCeilingX = (float)Math.Ceiling(radiusX);
            float radiusCeilingY = (float)Math.Ceiling(radiusY);
            float radiusCeilingZ = (float)Math.Ceiling(radiusZ);

            float midX = (diameterX - 1) / 2f;
            float midY = (diameterY - 1) / 2f;
            float midZ = (diameterZ - 1) / 2f;

            int diameterOffsetX = diameterX - 1;
            int diameterOffsetY = diameterY - 1;
            int diameterOffsetZ = diameterZ - 1;

            for (var x = 0; x < radiusCeilingX; ++x)
                for (var y = 0; y < radiusCeilingY; ++y)
                    for (var z = 0; z < radiusCeilingZ; ++z)
                        if (Maths.ElipsoidLocusCheck(x - midX, y - midY, z - midZ, radiusX, radiusY, radiusZ) &&
                            Maths.OnSurfaceEdgeCheck(x, y, z, (nx, ny, nz) => Maths.ElipsoidLocusCheck(nx - midX, ny - midY, nz - midZ, radiusX, radiusY, radiusZ)))
                        {
                            yield return new Block(x, y, z);
                            yield return new Block(diameterOffsetX - x, y, z);
                            yield return new Block(x, diameterOffsetY - y, z);
                            yield return new Block(x, y, diameterOffsetZ - z);
                            yield return new Block(diameterOffsetX - x, diameterOffsetY - y, z);
                            yield return new Block(x, diameterOffsetY - y, diameterOffsetZ - z);
                            yield return new Block(diameterOffsetX - x, y, diameterOffsetZ - z);
                            yield return new Block(diameterOffsetX - x, diameterOffsetY - y, diameterOffsetZ - z);
                        }
        }

        private void OutputSelectedLayers_CheckedChanged(object sender, EventArgs e)
        {
            fromLabel.Enabled = true;
            toLabel.Enabled = true;
            layerFrom.Enabled = true;
            layerTo.Enabled = true;
        }

        private void OutputAllLayers_CheckedChanged(object sender, EventArgs e)
        {
            fromLabel.Enabled = false;
            toLabel.Enabled = false;
            layerFrom.Enabled = false;
            layerTo.Enabled = false;

            layerFrom.Value = 1;
            layerTo.Value = diameterY.Value;
        }

        private void DiameterY_ValueChanged(object sender, EventArgs e)
        {
            layerFrom.Maximum = diameterY.Value;
            layerTo.Maximum = diameterY.Value;

            if (!layerTo.Enabled)
                layerTo.Value = diameterY.Value;
        }

        private bool ShowSizeWarning(int bmpSizeW, int bmpSizeH)
        {
            if (bmpSizeW > 15000 || bmpSizeH > 15000)
            {
                MessageBox.Show(
                    this,
                    String.Format("The image size will be too big to handle.{0}{0}Please lower the voxel size or diameter.{0}Or use the complex settings to produce the blueprints in sections.", Environment.NewLine),
                    "Image Too Big",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
                return true;
            }
            return false;
        }

        private static void SetColorPalette(Bitmap blueprint)
        {
            Color[] colours = new Color[] {
                // Backgrounds
                Color.White,
                Color.FromArgb(255 - 16, 255 - 16, 255 - 16),
                Color.FromArgb(255 - 16 * 2, 255 - 16 * 2, 255 - 16 * 2),
                Color.FromArgb(255 - 16 * 3, 255 - 16 * 3, 255 - 16 * 3),
                Color.FromArgb(255, 255, 0),
                Color.FromArgb(255 - 16, 255 - 16, 0),
                Color.FromArgb(255 - 16 * 2, 255 - 16 * 2, 0),
                Color.FromArgb(255 - 16 * 3, 255 - 16 * 3, 0),
                // Foregrounds
                Color.Black,
                Color.FromArgb(64, 64, 64)
            };

            ColorPalette palette = blueprint.Palette;

            for (int i = 0; i < colours.Length; i++)
                palette.Entries[i] = colours[i];

            blueprint.Palette = palette;
        }

        private void SimpleSave_Click(object sender, EventArgs e)
        {
            int diameter = (int)this.sphereDiameter.Value;
            int voxelSize = (int)this.voxelSize.Value;
            int wrap = (int)Math.Ceiling(Math.Sqrt(diameter));

            var bmpSize = diameter * voxelSize * wrap;

            if (ShowSizeWarning(bmpSize, bmpSize))
                return;

            SaveFileDialog sfd = new SaveFileDialog();
            sfd.Filter = "Bitmap (*.bmp)|*.bmp";

            if (sfd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
            {
                this.simpleSave.Enabled = false;

                BackgroundWorker worker = new BackgroundWorker();

                worker.DoWork += (wsender, we) =>
                {
                    var localWorker = (BackgroundWorker)wsender;

                    using (Bitmap blueprint = new Bitmap(diameter * voxelSize * wrap, diameter * voxelSize * wrap, PixelFormat.Format4bppIndexed))
                    {
                        int blueprintHeight = blueprint.Height;
                        int blueprintWidth = blueprint.Width;

                        SetColorPalette(blueprint);

                        var blueprintData = blueprint.LockBits(new Rectangle(0, 0, blueprintWidth, blueprintHeight), ImageLockMode.WriteOnly, blueprint.PixelFormat);

                        for (int x = 0; x < blueprintWidth; x++)
                            for (int y = 0; y < blueprintHeight; y++)
                            {
                                int colorIndex =
                                    (x / voxelSize / diameter + y / voxelSize / diameter) % 2 == 0 ?
                                        ((x / voxelSize + y / voxelSize) % 2 == 0 ?
                                            0 :
                                            1) :
                                        ((x / voxelSize + y / voxelSize) % 2 == 0 ?
                                            2 :
                                            3);

                                var centreX = x / voxelSize / diameter;
                                var centreY = y / voxelSize / diameter * wrap;
                                if (Math.Abs(centreX + centreY - (diameter / 2f) + 0.5f) < 1)
                                    colorIndex += 4;

                                Write4BitIndexedPixel(blueprintData, x, y, colorIndex);
                            }

                        var blocks = CreateElipsoid(diameter, diameter, diameter);
                        foreach (var block in blocks)
                        {
                            int xoffset = (block.Z % wrap) * diameter * voxelSize;
                            int yoffset = (block.Z / wrap) * diameter * voxelSize;

                            for (int i = 0; i < voxelSize; i++)
                                for (int j = 0; j < voxelSize; j++)
                                {
                                    int px = block.X * voxelSize + xoffset + i;
                                    int py = block.Y * voxelSize + yoffset + j;
                                    int colorIndex = (block.X + block.Y) % 2 + 8;

                                    Write4BitIndexedPixel(blueprintData, px, py, colorIndex);
                                }
                        }

                        blueprint.UnlockBits(blueprintData);

                        blueprint.Save(sfd.FileName);
                    }
                };

                worker.RunWorkerCompleted += (wsender, we) =>
                {
                    if (we.Error != null)
                        MessageBox.Show(this, String.Format("Sorry, an error occured.{0}{0}{1}", Environment.NewLine, we.Error), "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);

                    this.simpleSave.Enabled = true;
                };

                worker.RunWorkerAsync();
            }
        }

        private void ComplexSave_Click(object sender, EventArgs e)
        {
            int diameterX = (int)this.diameterX.Value;
            int diameterY = (int)this.diameterY.Value;
            int diameterZ = (int)this.diameterZ.Value;

            int layerFrom = (int)this.layerFrom.Value;
            int layerTo = (int)this.layerTo.Value;

            int voxelSize = (int)this.complexVoxelSize.Value;
            int wrap = (int)Math.Ceiling(Math.Sqrt(layerTo - layerFrom + 1));

            var bmpSizeW = diameterX * voxelSize * wrap;
            var bmpSizeH = diameterZ * voxelSize * wrap;

            if (ShowSizeWarning(bmpSizeW, bmpSizeH))
                return;

            SaveFileDialog sfd = new SaveFileDialog();
            sfd.Filter = "Bitmap (*.bmp)|*.bmp";

            if (sfd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
            {
                this.complexSave.Enabled = false;

                BackgroundWorker worker = new BackgroundWorker();

                worker.DoWork += (wsender, we) =>
                {
                    var localWorker = (BackgroundWorker)wsender;

                    using (Bitmap blueprint = new Bitmap(diameterX * voxelSize * wrap, diameterZ * voxelSize * wrap, PixelFormat.Format4bppIndexed))
                    {
                        int blueprintHeight = blueprint.Height;
                        int blueprintWidth = blueprint.Width;

                        SetColorPalette(blueprint);

                        var blueprintData = blueprint.LockBits(new Rectangle(0, 0, blueprintWidth, blueprintHeight), ImageLockMode.WriteOnly, blueprint.PixelFormat);

                        for (int x = 0; x < blueprintWidth; x++)
                            for (int y = 0; y < blueprintHeight; y++)
                            {
                                int colorIndex =
                                    (x / voxelSize / diameterX + y / voxelSize / diameterZ) % 2 == 0 ?
                                        ((x / voxelSize + y / voxelSize) % 2 == 0 ?
                                            0 :
                                            1) :
                                        ((x / voxelSize + y / voxelSize) % 2 == 0 ?
                                            2 :
                                            3);

                                var centreX = x / voxelSize / diameterX;
                                var centreY = y / voxelSize / diameterZ * wrap;
                                if (Math.Abs(centreX + centreY + layerFrom - (diameterY / 2f) - 0.5f) < 1)
                                    colorIndex += 4;

                                Write4BitIndexedPixel(blueprintData, x, y, colorIndex);
                            }

                        var blocks = CreateElipsoid(diameterX, diameterY, diameterZ);
                        foreach (var block in blocks)
                        {
                            if (block.Y < layerFrom - 1 || block.Y > layerTo - 1)
                                continue;

                            int xoffset = ((block.Y - layerFrom + 1) % wrap) * diameterX * voxelSize;
                            int yoffset = ((block.Y - layerFrom + 1) / wrap) * diameterZ * voxelSize;

                            for (int i = 0; i < voxelSize; i++)
                                for (int j = 0; j < voxelSize; j++)
                                {
                                    int px = block.X * voxelSize + xoffset + i;
                                    int py = block.Z * voxelSize + yoffset + j;
                                    int colorIndex = (block.X + block.Z) % 2 + 8;

                                    Write4BitIndexedPixel(blueprintData, px, py, colorIndex);
                                }
                        }

                        blueprint.UnlockBits(blueprintData);

                        blueprint.Save(sfd.FileName);
                    }
                };

                worker.RunWorkerCompleted += (wsender, we) =>
                {
                    if (we.Error != null)
                        MessageBox.Show(this, String.Format("Sorry, an error occured.{0}{0}{1}", Environment.NewLine, we.Error), "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);

                    this.complexSave.Enabled = true;
                };

                worker.RunWorkerAsync();
            }
        }
    }
}